-
Notifications
You must be signed in to change notification settings - Fork 8
Dev #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Dev #184
Conversation
Folder rearrange
disable init socket for externally deployed plugs
…into feat/trace
feat: hyper gas limti
…socket-protocol into fees-deposit-hook
Revert commented changes
feat: payload revert
Fees manager revert
Script and proxy changes
Fix expire bid and event in schedule precompile
Socket test
Clean storage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 50
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
contracts/evmx/helpers/AsyncPromise.sol (1)
144-155: Revert handler failure must not revert the whole flowcurrent behavior: If revertHandlerSelector is unset or the revert handler call fails, _handleRevert reverts. This rolls back state updates in markResolved/markOnchainRevert, bricking the promise and preventing retries. This should be non‑reverting with explicit state and/or events.
- function _handleRevert(bytes32 payloadId_) internal { - bytes memory combinedCalldata = abi.encodePacked( - revertHandlerSelector, - abi.encode(revertHandlerData, payloadId_) - ); - - (bool success, , ) = localInvoker.tryCall(0, gasleft(), 0, combinedCalldata); - - if (!success) { - // todo: in this case, promise will stay unresolved - revert PromiseRevertFailed(); - } - } + function _handleRevert(bytes32 payloadId_) internal { + // No handler set: leave state as *_REVERTING and return for retry logic. + if (revertHandlerSelector == bytes4(0)) { + return; + } + bytes memory combinedCalldata = abi.encodePacked( + revertHandlerSelector, + abi.encode(revertHandlerData, payloadId_) + ); + (bool success, , ) = localInvoker.tryCall(0, gasleft(), 0, combinedCalldata); + if (!success) { + // Do not revert; keep state in *_REVERTING to allow retries by watcher. + // Consider emitting an event here for observability. + return; + } + }Optional: emit events RevertHandlerCalled/RevertHandlerFailed for ops visibility.
hardhat-scripts/s3Config/buildConfig.ts (1)
32-33: Replace EVMX_CHAIN_ID with a valid ChainSlug
EVMX_CHAIN_ID is a numeric evmChainId from getCurrentModeConfig(), not a slug. Casting it to ChainSlug breaks rpcKeys/wssRpcKeys and all per-slug maps, leading to “Missing RPC” at runtime. Convert the chainId to its slug (or export an EVMX_CHAIN_SLUG) before using it in supportedChainSlugs and evmxChainSlug.contracts/evmx/fees/FeesManager.sol (1)
101-111: Over‑blocking bug: availability ignores already blocked creditsYou compare
balanceOf(consumeFrom_)tocredits_but ignoreuserBlockedCredits. This allows blocking more than the user’s free balance. This should enforceavailable = balance - blockedand prevent double‑blocking per request.function blockCredits( @@ - if (balanceOf(consumeFrom_) < credits_) revert InsufficientCreditsAvailable(); + uint256 balance = balanceOf(consumeFrom_); + uint256 blocked = userBlockedCredits[consumeFrom_]; + if (balance < blocked + credits_) revert InsufficientCreditsAvailable(); + // prevent double-block on the same request + if (requestBlockedCredits[requestCount_] != 0) revert AlreadyBlocked(); @@ - userBlockedCredits[consumeFrom_] += credits_; + userBlockedCredits[consumeFrom_] += credits_;And declare:
error AlreadyBlocked();contracts/evmx/base/AppGatewayBase.sol (1)
241-249: Approve on token contract, not FeesManager, before withdrawCreditsThis approve targets FeesManager as if it were a token. It must approve on token_ so FeesManager can pull funds.
- IERC20(address(feesManager__())).approve(address(feesManager__()), type(uint256).max); + IERC20(token_).approve(address(feesManager__()), type(uint256).max); feesManager__().withdrawCredits(chainSlug_, token_, amount_, maxFees, receiver_);hardhat-scripts/deploy/6.connect.ts (1)
142-165: Also validate switchboardId when building EVMx configsThis should prevent pushing invalid configs to the watcher.
- const switchboardId = addr[Contracts.FastSwitchboardId]; + const switchboardId = addr[Contracts.FastSwitchboardId]; + if (!switchboardId || switchboardId === "0") { + throw new Error(`FastSwitchboardId not found for chain ${chain}`); + }hardhat-scripts/admin/disconnect.ts (1)
49-56: Fix isConfigSetOnSocket argument: pass switchboardId (string), not addressThis should pass a switchboardId string. Using an address breaks the config check and causes unnecessary disconnects.
Apply:
- const switchboard = addr[Contracts.FastSwitchboard]; - checkIfAddressExists(switchboard, "Switchboard"); + const switchboard = addr[Contracts.FastSwitchboard]; + checkIfAddressExists(switchboard, "Switchboard"); + // Disconnected state uses switchboardId "0" + const switchboardId = "0"; ... - if ( - await isConfigSetOnSocket(plug, socket, BYTES32_ZERO, switchboard, chain) - ) { + if (await isConfigSetOnSocket(plug, socket, BYTES32_ZERO, switchboardId, chain)) { console.log(`${plugContract} Socket Config on ${chain} already set!`); return; }Based on learnings (utils/appConfig.ts signature).
contracts/evmx/watcher/RequestHandler.sol (1)
115-120: Guard against invalid callType in getPrecompileFeesCalling an interface on address(0) will revert with a low-level error. This should validate the precompile exists and revert with a domain error.
function getPrecompileFees( bytes4 callType_, bytes memory precompileData_ ) external view returns (uint256) { - return precompiles[callType_].getPrecompileFees(precompileData_); + IPrecompile pc = precompiles[callType_]; + if (address(pc) == address(0)) revert InvalidCallType(); + return pc.getPrecompileFees(precompileData_); }contracts/evmx/fees/Credit.sol (3)
137-152: deposit: double-mint bug and wrong credit/native accounting on withdraw failureOn withdraw failure, this mints creditAmount_ again, zeroes nativeAmount_, then adds zero to creditAmount_. This should mint nativeAmount_ credits instead and reflect in event values.
- // Mint tokens to the user - _mint(depositTo_, creditAmount_); + // Mint credits to the user + _mint(depositTo_, creditAmount_); ... - if (!success) { - _mint(depositTo_, creditAmount_); - nativeAmount_ = 0; - creditAmount_ += nativeAmount_; - } + if (!success) { + // Compensate native payout with credits + _mint(depositTo_, nativeAmount_); + creditAmount_ += nativeAmount_; + nativeAmount_ = 0; + }Ensure tokenOnChainBalances semantics remain consistent with how payouts are later debited.
198-209: Allowance check must ensure allowance >= amount, not just > 0Current isCreditSpendable only checks allowance != 0. This should check allowance >= amount_ to avoid later revert inside super.transferFrom.
- if (!_isWatcher(spender_) && consumeFrom_ != spender_) { - if (allowance(consumeFrom_, spender_) == 0) return false; - } + if (!_isWatcher(spender_) && consumeFrom_ != spender_) { + if (allowance(consumeFrom_, spender_) < amount_) return false; + }
271-283: createRequest ignores maxFees argumentmaxFees passed by withdrawCredits is unused. This should set overrides with the provided maxFees_ (or the min of user cap and per-chain cap), not always getMaxFees(chainSlug_).
- _setMaxFees(getMaxFees(chainSlug_)); + _setMaxFees(maxFees_);If you need a chain cap, enforce min(maxFees_, getMaxFees(chainSlug_)).
♻️ Duplicate comments (39)
hardhat-scripts/deploy/2.roles.ts (1)
99-103: WATCHER_ROLE not granted to MessageSwitchboard (bug).The grant logic omits
MessageSwitchboardfrom the condition that routes WATCHER_ROLE to the watcher address. This causes WATCHER_ROLE to be incorrectly granted tosigner.addressfor MessageSwitchboard.Apply this diff:
- [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard].includes( + [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard, Contracts.MessageSwitchboard].includes( contractName as Contracts ) && roleName === ROLES.WATCHER_ROLEsrc/finality.ts (1)
7-7: Past LOW=0 concern not addressedThe previous review flagged that
LOW: 0creates reorg exposure and falsy-defaulting bugs. This change from 1→0 remains unaddressed. Either document this as intentional "head-of-chain" semantics with explicit consumer guidance to use??instead of||, or revert to 1 for safer defaults.hardhat-scripts/verify/verify.ts (1)
119-135: Retry loop still re-verifies all contracts and sleeps unconditionally.The issues flagged in past review comments remain:
- The loop doesn't narrow the pending set between retries—it keeps re-verifying
chainParamsinstead ofunverifiedChainParams.- The sleep happens before checking if verification is complete.
Since the loop currently runs only once (line 119), these issues don't cause problems yet. However, if you increase the retry limit in the future, apply the fixes from the past review comments to avoid unnecessary work and delays.
hardhat-scripts/constants/types.ts (1)
17-17: Type switchboardId as BigNumberish for uint64 safety.As noted in previous review, using
stringfor a uint64 value is too permissive and can cause runtime errors.BigNumberishprovides proper type safety for ethers.js calls.Apply this diff:
+import { BigNumberish } from "ethers"; + export type AppGatewayConfig = { plugConfig: { appGatewayId: string; - switchboardId: string; + switchboardId: BigNumberish; }; plug: string; chainSlug: number; };hardhat-scripts/utils/address.ts (2)
58-65: Simplify toBytes32Format.The function redundantly strips 0x prefix when
toBytes32FormatHexStringshould always return a 0x-prefixed string after fixing the previous issue.Apply this diff:
export function toBytes32Format(hexString: string): number[] { - const hex32Format = toBytes32FormatHexString(hexString); - const cleanedHex32String = hex32Format.startsWith("0x") - ? hex32Format.slice(2) - : hex32Format; - - return Array.from(Buffer.from(cleanedHex32String, "hex")); + const hex32 = toBytes32FormatHexString(hexString); + return Array.from(Buffer.from(hex32.slice(2), "hex")); }
42-56: Fix padding/validation for toBytes32FormatHexString.The function has multiple edge-case issues:
- Odd-length hex produces fractional offset, silently dropping nibbles
- Non-hex input isn't validated
- Inputs longer than 32 bytes aren't checked
- Returns input unchanged for 64-char strings without ensuring 0x prefix
This should strictly validate hex, left-pad to 32 bytes, and always return 0x-prefixed output.
Apply this diff:
-export function toBytes32FormatHexString(hexString: string): string { - // this means that the string is already in bytes32 format with or without 0x prefix - if (hexString.length == 64 || hexString.length == 66) { - return hexString; - } - // Remove the '0x' prefix from the input string if it's present - const cleanedHexString = hexString.startsWith("0x") - ? hexString.slice(2) - : hexString; - - const buffer = Buffer.alloc(32); - buffer.write(cleanedHexString, 32 - cleanedHexString.length / 2, "hex"); // each hex char is 2 bytes - - return "0x" + buffer.toString("hex"); -} +export function toBytes32FormatHexString(input: string): string { + // Normalize and strip 0x + let hex = input.startsWith("0x") ? input.slice(2) : input; + // Validate hex + if (!/^[0-9a-fA-F]*$/.test(hex)) { + throw new Error(`Invalid hex string: ${input}`); + } + // Enforce max size + if (hex.length > 64) { + throw new Error(`Hex string too long for bytes32 (got ${hex.length} nybbles)`); + } + // Enforce even length + if (hex.length % 2 !== 0) { + hex = "0" + hex; + } + // Left-pad to 32 bytes (64 nybbles) + return "0x" + hex.padStart(64, "0").toLowerCase(); +}contracts/protocol/switchboard/SwitchboardBase.sol (1)
38-40: Add duplicate registration guard and event emission.
registerSwitchboardcan be called multiple times, overwritingswitchboardIdwithout validation or notification. This should prevent duplicate registrations and emit an event for off-chain tracking.Apply this diff:
+ event SwitchboardRegistered(uint64 indexed switchboardId); + function registerSwitchboard() external onlyOwner { + require(switchboardId == 0, "Already registered"); switchboardId = socket__.registerSwitchboard(); + emit SwitchboardRegistered(switchboardId); }hardhat-scripts/constants/constants.ts (1)
14-15: Document units for MSG_SB_FEES (keep string BigNumberish)This should state wei units to prevent misuse. Keep string for BigNumberish compatibility.
-export const MSG_SB_FEES = "100000000"; +// Switchboard sibling-config fee (wei); keep as string for BigNumberish compatibility. +export const MSG_SB_FEES = "100000000";contracts/protocol/interfaces/ISwitchboard.sol (1)
19-26: Fix param docs order and document payable rationaleDocs list triggerId_ before plug_, but signature takes plug_ first. This should match the function to avoid integration errors. Also add a clear note why this is payable (fees/gas).
- /** - * @notice Processes a trigger and creates payload - * @dev This function is called by the socket to process a trigger - * @dev sb can override this function to add additional logic - * @param triggerId_ Trigger ID from socket - * @param plug_ Source plug address - * @param payload_ Payload data - * @param overrides_ Overrides for the trigger - */ + /** + * @notice Processes a trigger and creates payload. + * @dev Called by Socket; switchboards can implement custom logic. + * @dev Payable to collect switchboard fees and/or gas prepayment; implementers must document exact requirements. + * @param plug_ Source plug address + * @param triggerId_ Trigger ID from Socket + * @param payload_ Payload data + * @param overrides_ Overrides for the trigger + */package.json (1)
25-25: Guard .env and avoid relying on executable bitThe trace script will fail when .env is absent and is shell‑specific. This should guard the source and invoke via bash for portability.
- "trace": "source .env && bash trace.sh", + "trace": "bash -lc '[[ -f .env ]] && source .env; bash trace.sh'",hardhat-scripts/utils/overrides.ts (1)
7-7: defaultType still forces legacy transactions (type 0).The hardcoded
defaultType = 0at Line 7 forces legacy transactions on all networks where an explicit type isn't set. This prevents EIP-1559 transactions on networks that prefer or require them. The getOverrides function at Line 112 still uses this default.Remove the default type to let ethers infer the appropriate transaction type:
-const defaultType = 0; - const { gasLimit, type = defaultType } = overrides; + const { gasLimit, type } = overrides;Also applies to: 112-112
script/counter/DeployEVMxCounterApp.s.sol (1)
19-19: Fees remain hardcoded at 1 ether.This was previously flagged. Fees should be sourced from an environment variable (e.g., FEES_WEI) to avoid misconfiguration across networks/forks.
- uint256 fees = 1 ether; + uint256 fees = vm.envUint("FEES_WEI");contracts/protocol/SocketConfig.sol (1)
123-132: Address the interface mismatch in connect() function.The past review comment is still valid. The function signature doesn't match the ISocket interface. The interface expects
connect(bytes32 appGatewayId_, address socket_, uint64 switchboardId_)but this implementation omits thesocket_parameter.Apply this diff to fix the signature:
- function connect(bytes32 appGatewayId_, uint64 switchboardId_) external override { + function connect(bytes32 appGatewayId_, address socket_, uint64 switchboardId_) external override { if (isValidSwitchboard[switchboardId_] != SwitchboardStatus.REGISTERED) revert InvalidSwitchboard(); - PlugConfigEvm storage _plugConfig = _plugConfigs[msg.sender]; + PlugConfigEvm storage _plugConfig = _plugConfigs[socket_]; _plugConfig.appGatewayId = appGatewayId_; _plugConfig.switchboardId = switchboardId_; - emit PlugConnected(msg.sender, appGatewayId_, switchboardId_); + emit PlugConnected(socket_, appGatewayId_, switchboardId_); }contracts/evmx/watcher/Watcher.sol (1)
183-201: tryCall error handling loses revert reason.The past review comment correctly identified that the current implementation only checks the success boolean and discards the returnData from tryCall (lines 193-199). This makes debugging failures difficult since the actual revert reason from the called contract is lost.
test/evmx/AuctionManager.t.sol (1)
33-51: Test lacks outcome validation.This test executes operations but doesn't verify results. The past review comments correctly identified:
- Missing balance/state checks after withdrawCredits (lines 39-46)
- Missing assertions after executeRequest calls to verify bid expiry and state changes (lines 47-51)
Without assertions, the test can't detect failures in the withdrawal or bid expiry logic.
script/helpers/TransferRemainingCredits.s.sol (1)
28-37: Missing validation before transfer.The past review comment correctly identified that the script doesn't check if
availableFees > 0before attempting the transfer (lines 28-37). This results in unnecessary transaction costs for no-op transfers.contracts/protocol/switchboard/FastSwitchboard.sol (2)
43-43: Changing visibility from external to public increases gas; keep external (or external virtual)attest is only used externally; public adds unnecessary ABI thunk. This should be external (and virtual if overriding).
- function attest(bytes32 digest_, bytes calldata proof_) public virtual { + function attest(bytes32 digest_, bytes calldata proof_) external virtual {
64-72: Empty processTrigger silently accepts ETH and does nothingAn external payable function with an empty body will consume gas/ETH and do nothing. This should be implemented, made abstract, or revert explicitly to prevent fund loss and logic gaps.
Options:
- function processTrigger( + function processTrigger( address plug_, bytes32 triggerId_, bytes calldata payload_, bytes calldata overrides_ - ) external payable virtual {} + ) external payable virtual { + revert("NotImplemented"); + }Or mark the contract abstract and declare the function without a body if derived contracts must implement it.
contracts/evmx/fees/FeesManager.sol (1)
51-67: Reinitializer bump requires an explicit upgrade/migration path
reinitializer(2)indicates an upgrade. Existing deployments will skip v1 initializer; new state must be safely initialized and storage layout preserved. This should include migration logic, upgrade tests, and documentation.Add/point to:
- Upgrade script that calls the new initializer post‑proxy upgrade.
- A test that deploys v1, upgrades to v2, validates
maxFeesPerChainSlug,sbType,feesPool, and owner/app gateway state.contracts/protocol/SocketBatcher.sol (3)
13-19: Move IFastSwitchboard to interfaces fileInterfaces should live under interfaces/ for reuse and discoverability.
Create contracts/protocol/interfaces/IFastSwitchboard.sol and import it here.
47-66: Validate switchboardId resolves to a non-zero address before attestThis should guard against invalid IDs and clearer errors.
- IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(digest_, proof_); + address switchboard = socket__.switchboardAddresses(switchboardId_); + if (switchboard == address(0)) revert InvalidSwitchboard(); + IFastSwitchboard(switchboard).attest(digest_, proof_);
76-104: CCTP: add zero-address check, try/catch around attest/prove, and syncOut only on successThis should improve safety and diagnostics; syncing on failure risks inconsistency.
- address switchboard = socket__.switchboardAddresses(switchboardId_); + address switchboard = socket__.switchboardAddresses(switchboardId_); + if (switchboard == address(0)) revert InvalidSwitchboard(); ... - ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions( - execParams_, - cctpParams_, - payloadId - ); + try ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions( + execParams_, + cctpParams_, + payloadId + ) { + // ok + } catch Error(string memory reason) { + revert(string.concat("CCTP attest/prove failed: ", reason)); + } catch { + revert("CCTP attest/prove failed"); + } ... - ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs); - return (success, returnData); + if (success) { + ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs); + } + return (success, returnData);contracts/evmx/base/AppGatewayBase.sol (3)
125-130: Validate request count in onDeployComplete to prevent TOCTOUYou encode watcher__().getCurrentRequestCount() but ignore it in the callback. This should validate the request count to avoid races between deploy and completion.
Apply:
- function onDeployComplete(uint40, bytes calldata onCompleteData_) external virtual onlyWatcher { + function onDeployComplete(uint40 requestCount_, bytes calldata onCompleteData_) external virtual onlyWatcher { + require(requestCount_ <= watcher__().getCurrentRequestCount(), "Invalid request count"); if (onCompleteData_.length == 0) return; uint32 chainSlug = abi.decode(onCompleteData_, (uint32)); initializeOnChain(chainSlug); }Also applies to: 373-377
145-150: Validate decoded deployed address before converting to bytes32This should reject address(0) before toBytes32Format to avoid storing invalid forwarders.
function setAddress(bytes memory data_, bytes memory returnData_) external onlyPromises { (uint32 chainSlug, bytes32 contractId) = abi.decode(data_, (uint32, bytes32)); - forwarderAddresses[contractId][chainSlug] = asyncDeployer__().getOrDeployForwarderContract( - toBytes32Format(abi.decode(returnData_, (address))), - chainSlug - ); + address deployed = abi.decode(returnData_, (address)); + require(deployed != address(0), "Invalid deployed address"); + forwarderAddresses[contractId][chainSlug] = asyncDeployer__().getOrDeployForwarderContract( + toBytes32Format(deployed), + chainSlug + ); }
226-228: Guard against zero on-chain plug addressRegistering a zero plug degrades watcher state and later calls.
function _setValidPlug(bool isValid, uint32 chainSlug_, bytes32 contractId_) internal { bytes32 onchainAddress = getOnChainAddress(contractId_, chainSlug_); + require(onchainAddress != bytes32(0), "Invalid onchain address"); watcher__().setIsValidPlug(isValid, chainSlug_, onchainAddress); }hardhat-scripts/deploy/3.configureChains.ts (2)
50-57: Do not overwrite existing SwitchboardIdToAddressMapThis resets mappings and loses prior state. Initialize only when undefined.
- let deployUtils: DeployParams = { + let deployUtils: DeployParams = { addresses: chainAddresses, mode, signer: signer, currentChainSlug: chain as ChainSlug, }; - deployUtils.addresses.SwitchboardIdToAddressMap = {}; + deployUtils.addresses.SwitchboardIdToAddressMap = + deployUtils.addresses.SwitchboardIdToAddressMap || {};
61-72: Validate returned fastSwitchboardId before storingThis should fail fast if registration/query returns zero/invalid.
const fastSwitchboardId = await registerSb( chain, chainAddresses[Contracts.FastSwitchboard], signer, socketContract ); + if (!fastSwitchboardId || fastSwitchboardId.toString() === "0") { + throw new Error(`Failed to register FastSwitchboard for chain ${chain}`); + } deployUtils.addresses[Contracts.FastSwitchboardId] = fastSwitchboardId.toString();hardhat-scripts/deploy/6.connect.ts (1)
48-56: Validate switchboardId before using itThis should reject missing/zero IDs and prevent bad socket config calls.
- const switchboardId = addr[Contracts.FastSwitchboardId]; + const switchboardId = addr[Contracts.FastSwitchboardId]; + if (!switchboardId || switchboardId === "0") { + throw new Error(`FastSwitchboardId not found for chain ${chain}`); + } ... const tx = await plug.functions[functionName]( appGatewayId, socket.address, switchboardId,Also applies to: 68-73
contracts/evmx/AuctionManager.sol (1)
235-246: then() inside async-wrapped function attaches to the previous promise.With
internal async,_postAsync()creates the new Promise after the function body. Callingthen(...)here registers on the old promise.Fix one of:
- Move
then(callbackSelector_, callbackData_)to be called immediately after invoking_createRequest(...)at the call sites.- Or extend the
asyncmodifier/post-hook to accept(selector, data)and callIPromise(watcher__().latestAsyncPromise()).then(selector, data)inside_postAsync().Minimal change:
- _setSchedule(delayInSeconds_); - then(callbackSelector_, callbackData_); + _setSchedule(delayInSeconds_); + // Attach .then(...) to the newly created promise after this function returns.hardhat-scripts/deploy/1.deploy.ts (1)
122-124: Fix optional chaining check for feePool (regression).
feePool?.length == 0skips deployment whenfeePoolis undefined. This should deploy when missing or empty.- const feePool = getFeePool(); - if (feePool?.length == 0) { + const feePool = getFeePool(); + if (!feePool || feePool.length === 0) {Errors.md (1)
52-56: Doc inconsistency: removed error name mismatch.Summary claims removal of
SwitchboardExistsOrDisabled(), table listsSwitchboardExists(). This should be consistent.Update either the summary or the table so both reference the same removed error name/signature.
contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)
183-186: Validate and mask payloadPointer-derived fields before use.Direct shifts to
uint40(...)without masking/validation can yield garbage and corruptprevBatchDigestHash.- bytes32 prevBatchDigestHash = getPrevBatchDigestHash( - uint40(payloadParams.payloadPointer >> 120), - uint40(payloadParams.payloadPointer >> 80) - ); + uint40 requestCount = uint40((payloadParams.payloadPointer >> 120) & bytes32(uint256((1 << 40) - 1))); + uint40 batchCount = uint40((payloadParams.payloadPointer >> 80) & bytes32(uint256((1 << 40) - 1))); + if (requestCount == 0) revert InvalidIndex(); // or a dedicated error + // Optionally validate batchCount against known bounds + bytes32 prevBatchDigestHash = getPrevBatchDigestHash(requestCount, batchCount);Pick the appropriate error type for out-of-range values (e.g., a dedicated
InvalidRequestCount()).test/evmx/FeesTest.t.sol (1)
96-103: Add explicit post-disconnect assertionsThis should assert the plug is disconnected before proceeding (e.g., getPlugConfig returns appGatewayId == 0). Without it, the test could pass due to unrelated conditions.
Apply this snippet after disconnectSocket():
hoax(socketOwner); arbConfig.feesPlug.disconnectSocket(); + +// Verify disconnection state +{ + (bytes32 appGatewayId_, uint64 switchboardId_) = + arbConfig.socket.getPlugConfig(address(arbConfig.feesPlug)); + assertEq(appGatewayId_, bytes32(0), "appGatewayId should be zero after disconnect"); + // If protocol semantics keep switchboardId, assert unchanged; else enforce zero: + // assertEq(switchboardId_, 0, "switchboardId should be zero after disconnect"); +}contracts/utils/common/Structs.sol (1)
213-219: Bound dynamic arrays to prevent DoS in consumersCCTPBatchParams and SolanaInstruction* structs expose unbounded arrays. This should have enforced limits in consuming contracts (batch sizes, accounts, args) with clear reverts to prevent gas griefing.
- Define MAX_BATCH_SIZE, MAX_ACCOUNTS, MAX_ARGUMENTS constants and validate in entry points that accept these structs.
- Validate related lengths (e.g., attestations/messages alignment, accountFlags length == accounts length, functionArgumentTypeNames length == functionArguments length).
Also applies to: 220-239
test/SetupTest.t.sol (2)
1158-1173: setupGatewayAndPlugs: remove double-iteration; build configs in one passThis currently scans contractIds_ twice. Build configs once into a temp array and resize.
- uint256 validPlugCount = _countValidPlugs(appGateway_, contractIds_, chainSlug_); - AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount); - _populateConfigs(configs, appGateway_, contractIds_, chainSlug_); - if (validPlugCount > 0) { - watcherMultiCall( - address(configurations), - abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) - ); - } + AppGatewayConfig[] memory tmp = new AppGatewayConfig[](contractIds_.length); + uint256 count = 0; + uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType()); + for (uint i = 0; i < contractIds_.length; i++) { + bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); + if (plug != bytes32(0)) { + tmp[count++] = AppGatewayConfig({ + plug: plug, + chainSlug: chainSlug_, + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(appGateway_)), + switchboardId: switchboardId + }) + }); + } + } + if (count > 0) { + AppGatewayConfig[] memory configs = new AppGatewayConfig[](count); + for (uint i = 0; i < count; i++) configs[i] = tmp[i]; + watcherMultiCall( + address(configurations), + abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) + ); + }Also applies to: 1175-1186, 1188-1211
1105-1108: Fix abi.decode type: last tuple element is uint64 (switchboardId), not addressThis currently causes a type mismatch and/or incorrect decoding.
- (, Transaction memory transaction, , , , ) = abi.decode( - params.precompileData, - (address, Transaction, WriteFinality, uint256, uint256, address) - ); + (, Transaction memory transaction, , , , ) = abi.decode( + params.precompileData, + (address, Transaction, WriteFinality, uint256, uint256, uint64) + );contracts/evmx/watcher/RequestHandler.sol (2)
255-258: payloadPointer packing can overflow allocated bit rangesrequestCount_ and nextBatchCount must fit 40 bits; payloadCounter must fit 80 bits. Without checks/masking, fields can overflow and corrupt the pointer.
+ // Ensure values fit within 40/40/80 bits respectively + require(requestCount_ < (1 << 40), "Request count overflow"); + require(nextBatchCount < (1 << 40), "Batch count overflow"); + require(payloadCounter < (1 << 80), "Payload counter overflow"); - uint160 payloadPointer = (uint160(requestCount_) << 120) | - (uint160(nextBatchCount) << 80) | - uint160(payloadCounter++); + uint160 payloadPointer = + (uint160(requestCount_) << 120) | + (uint160(nextBatchCount) << 80) | + uint160(payloadCounter++);
428-431: Capture and emit failure data from tryCall in onCompleteOnly emitting RequestCompletedWithErrors loses error context. This should capture returnData and emit it.
- if (r.onCompleteData.length > 0) { - (bool success, , ) = r.appGateway.tryCall(0, gasleft(), 0, r.onCompleteData); - if (!success) emit RequestCompletedWithErrors(requestCount_); - } + if (r.onCompleteData.length > 0) { + (bool success, bytes memory returnData, ) = + r.appGateway.tryCall(0, gasleft(), 0, r.onCompleteData); + if (!success) { + // Consider updating the event to include bytes returnData + emit RequestCompletedWithErrors(requestCount_); + // Optionally emit returnData via a new event to aid debugging + } + }EventTopics.md (1)
41-45: Empty sections should be removed or populated.These empty interface sections (IFastSwitchboard, IMessageSwitchboard, ICCTPSwitchboard) have been flagged in previous reviews. If these interfaces have no events to document, remove the sections; otherwise, populate them with the actual event information.
| function _permit(bytes memory feesApprovalData_) internal { | ||
| if (feesApprovalData_.length == 0) return; | ||
| (consumeFrom, , ) = feesManager__().approveAppGatewayWithSignature(feesApprovalData_); | ||
| (address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes)); | ||
| IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_permit calls permit on FeesManager instead of token; decode token and target ERC20
Calling IERC20(address(feesManager__())).permit(...) is incorrect. Approval must be executed on the ERC20 token contract. Include token in feesApprovalData_ and call permit on that token.
function _permit(bytes memory feesApprovalData_) internal {
if (feesApprovalData_.length == 0) return;
- (address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes));
- IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature);
+ (address token, address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature)
+ = abi.decode(feesApprovalData_, (address, address, uint256, uint256, uint256, bytes));
+ IERC20(token).permit(spender, value, deadline, nonce, signature);
}Note: If your IERC20 doesn’t define permit, use your ERC20Permit interface instead. Based on learnings
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function _permit(bytes memory feesApprovalData_) internal { | |
| if (feesApprovalData_.length == 0) return; | |
| (consumeFrom, , ) = feesManager__().approveAppGatewayWithSignature(feesApprovalData_); | |
| (address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes)); | |
| IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature); | |
| } | |
| function _permit(bytes memory feesApprovalData_) internal { | |
| if (feesApprovalData_.length == 0) return; | |
| (address token, address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) | |
| = abi.decode(feesApprovalData_, (address, address, uint256, uint256, uint256, bytes)); | |
| IERC20(token).permit(spender, value, deadline, nonce, signature); | |
| } |
🤖 Prompt for AI Agents
In contracts/evmx/base/AppGatewayBase.sol around lines 230-234, the _permit
function wrongly calls permit on the FeesManager; change the ABI decode to
include the token address (e.g., decode (address token, address spender, uint256
value, uint256 deadline, uint256 nonce, bytes memory signature)), then cast the
decoded token address to your ERC20Permit interface and call permit on that
token (not on feesManager__), keeping the early-return when
feesApprovalData_.length == 0; also use ERC20Permit type (or your IERC20 with
permit) to ensure the permit method exists.
| function decimals() external view returns (uint8); | ||
|
|
||
| function permit( | ||
| address spender, | ||
| uint256 value, | ||
| uint256 deadline, | ||
| uint256 nonce, | ||
| bytes memory signature | ||
| ) external; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non‑standard permit in IERC20 will break ERC‑2612 compatibility
This interface mixes core ERC‑20 with a non‑standard permit(address spender, uint256 value, uint256 deadline, uint256 nonce, bytes signature). ERC‑2612 defines:
permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)- plus
nonces(address)andDOMAIN_SEPARATOR().
Calling external tokens with this signature will revert. This should split into IERC20 (transfer/approve/etc.) and IERC20Permit (EIP‑2612), or drop permit here and use a separate interface.
Apply this diff to remove the non‑standard method:
interface IERC20 {
@@
- function permit(
- address spender,
- uint256 value,
- uint256 deadline,
- uint256 nonce,
- bytes memory signature
- ) external;Add a separate IERC20Permit (new file) aligned to EIP‑2612:
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;
interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
}🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IERC20.sol around lines 17 to 25, the interface
currently declares a non‑standard permit(spender,value,deadline,nonce,signature)
which breaks ERC‑2612 compatibility; remove that non‑standard permit declaration
from IERC20 so it only contains core ERC‑20 methods (decimals, transfer,
approve, allowance, balanceOf, totalSupply, etc.), and add a new file
contracts/evmx/interfaces/IERC20Permit.sol implementing the EIP‑2612 signature
(permit(owner,spender,value,deadline,v,r,s)) plus nonces(address) and
DOMAIN_SEPARATOR() as view functions; ensure SPDX/license and pragma lines match
project style and update any imports/usages to reference IERC20Permit where
EIP‑2612 behavior is required.
| requestCount = uint40(payloadParams.payloadPointer >> 120); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Duplicated bit extraction and no bounds check on requestCount
requestCount = uint40(payloadParams.payloadPointer >> 120) is repeated and relies on a specific bit layout. Silent truncation to uint40 can hide overflow or layout drift. This should centralize decoding and validate the range.
- requestCount = uint40(payloadParams.payloadPointer >> 120);
+ requestCount = _extractRequestCount(payloadParams.payloadPointer);
...
- requestHandler__().cancelRequestForReverts(uint40(payloadParams.payloadPointer >> 120));
+ requestHandler__().cancelRequestForReverts(_extractRequestCount(payloadParams.payloadPointer));Add helper (outside the ranges shown):
function _extractRequestCount(uint256 payloadPointer) internal pure returns (uint40) {
unchecked {
uint256 shifted = payloadPointer >> 120; // [40 bits: requestCount][120 bits: rest]
require(shifted <= type(uint40).max, "requestCount overflow");
return uint40(shifted);
}
}Confirm the payloadPointer bit layout is stable and documented. If layout differs across versions, store requestCount explicitly rather than packing it.
Also applies to: 96-97
🏁 Script executed:
#!/bin/bash
# Locate duplicated bit extraction in PromiseResolver.sol
rg -n "uint40(payloadParams.payloadPointer >> 120)" -C2 contracts/evmx/watcher/PromiseResolver.sol
# Search for existing helper function named extractRequestCount
rg -n "extractRequestCount" -C2 .Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find the location of PromiseResolver.sol
fd PromiseResolver.sol
# Search for the bit extraction pattern more broadly
rg -n "payloadPointer >> 120" -C2Length of output: 2212
Centralize requestCount extraction and add range check
Replace duplicated uint40(payloadParams.payloadPointer >> 120) at lines 65 and 96 with a helper and validate overflow:
function _extractRequestCount(uint256 payloadPointer) internal pure returns (uint40) {
unchecked {
uint256 shifted = payloadPointer >> 120;
require(shifted <= type(uint40).max, "requestCount overflow");
return uint40(shifted);
}
}Use _extractRequestCount(payloadParams.payloadPointer) for both the requestCount assignment and the cancelRequestForReverts call.
🤖 Prompt for AI Agents
In contracts/evmx/watcher/PromiseResolver.sol around lines 65 to 66 (and also
update the duplicate at line 96), the extraction of requestCount via repeated
uint40(payloadParams.payloadPointer >> 120) should be centralized: add an
internal pure helper named _extractRequestCount(uint256 payloadPointer) that
shifts payloadPointer right by 120, checks the shifted value does not exceed
type(uint40).max (require with "requestCount overflow"), and returns
uint40(shifted) (use unchecked for the cast), then replace the inline
uint40(...) at line 65 and the duplicate at line 96 with a call to
_extractRequestCount(payloadParams.payloadPointer).
| if (isAppGatewayCalled[params_.triggerId]) revert AppGatewayAlreadyCalled(); | ||
|
|
||
| address appGateway = decodeAppGatewayId(params_.appGatewayId); | ||
| address appGateway = fromBytes32Format(params_.appGatewayId); | ||
| if (!configurations__.isValidPlug(appGateway, params_.chainSlug, params_.plug)) | ||
| revert InvalidCallerTriggered(); | ||
|
|
||
| feesManager__().transferCredits(appGateway, address(this), triggerFees); | ||
| IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees); | ||
|
|
||
| triggerFromChainSlug = params_.chainSlug; | ||
| triggerFromPlug = params_.plug; | ||
| isAppGatewayCalled[params_.triggerId] = true; | ||
| (bool success, , ) = appGateway.tryCall( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix reentrancy/double-charge and enforce ERC20 transfer success
Setting isAppGatewayCalled only on success enables retries and allows reentrancy during appGateway.call to re-enter and double-charge. This should set the flag before the external call. Also, ERC20 transferFrom return value is ignored; tokens that return false won’t revert. Enforce success.
function _callAppGateways(TriggerParams memory params_) internal {
if (isAppGatewayCalled[params_.triggerId]) revert AppGatewayAlreadyCalled();
address appGateway = fromBytes32Format(params_.appGatewayId);
if (!configurations__.isValidPlug(appGateway, params_.chainSlug, params_.plug))
revert InvalidCallerTriggered();
- IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees);
+ bool ok = IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees);
+ if (!ok) revert TransferFailed(); // define TransferFailed() error in scope
triggerFromChainSlug = params_.chainSlug;
triggerFromPlug = params_.plug;
+ // prevent reentrancy/retries before external call
+ isAppGatewayCalled[params_.triggerId] = true;
(bool success, , ) = appGateway.tryCall(
0,
gasleft(),
0, // setting max_copy_bytes to 0 as not using returnData right now
params_.payload
);
if (!success) {
emit TriggerFailed(params_.triggerId);
} else {
- isAppGatewayCalled[params_.triggerId] = true;
emit TriggerSucceeded(params_.triggerId);
}
triggerFromChainSlug = 0;
triggerFromPlug = bytes32(0);
}If you prefer to support non‑standard ERC20s, replace the bool check with a safe wrapper (e.g., solady’s SafeTransferLib.safeTransferFrom).
Also applies to: 48-57
🤖 Prompt for AI Agents
In contracts/evmx/watcher/Trigger.sol around lines 31-41 (and similarly lines
48-57), the reentrancy/double-charge issue and unchecked ERC20 transfer are
present: set isAppGatewayCalled[params_.triggerId] = true before making the
external appGateway.call to prevent reentrancy and double charging, and enforce
that IERC20(feesManager__()).transferFrom(...) succeeds by checking the returned
bool (or use a safe transfer wrapper such as
SafeTransferLib.safeTransferFrom/solady) and revert on failure; apply the same
ordering and transfer success check in the other occurrence at lines 48-57.
| /// @notice Initializes the socket | ||
| /// @dev this function should be called even if deployed independently | ||
| /// to avoid ownership and permission exploit | ||
| /// @param appGatewayId_ The app gateway id | ||
| /// @param socket_ The socket address | ||
| /// @param switchboardId_ The switchboard id | ||
| function initSocket( | ||
| bytes32 appGatewayId_, | ||
| address socket_, | ||
| address switchboard_ | ||
| uint64 switchboardId_ | ||
| ) external virtual socketInitializer { | ||
| _connectSocket(appGatewayId_, socket_, switchboard_); | ||
| _connectSocket(appGatewayId_, socket_, switchboardId_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initSocket lacks access control; first caller can permanently bind the plug
initSocket is external and only gated by socketInitializer. Any account can call it first and set socket_/appGatewayId_/switchboardId_, hijacking the plug. This should be owner/role‑gated or internal.
Options:
- Gate with ownership/roles (preferred):
- function initSocket(
+ function initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
- ) external virtual socketInitializer {
+ ) external virtual socketInitializer onlyOwner {
_connectSocket(appGatewayId_, socket_, switchboardId_);
}Add an ownership base (e.g., Ownable2Step/OwnableRoles) in this contract or ensure every derived contract applies onlyOwner.
- Or make it internal so derived contracts expose a guarded initializer:
- function initSocket(
+ function _initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
- ) external virtual socketInitializer {
+ ) internal virtual socketInitializer {
_connectSocket(appGatewayId_, socket_, switchboardId_);
}This should be fixed before release.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// @notice Initializes the socket | |
| /// @dev this function should be called even if deployed independently | |
| /// to avoid ownership and permission exploit | |
| /// @param appGatewayId_ The app gateway id | |
| /// @param socket_ The socket address | |
| /// @param switchboardId_ The switchboard id | |
| function initSocket( | |
| bytes32 appGatewayId_, | |
| address socket_, | |
| address switchboard_ | |
| uint64 switchboardId_ | |
| ) external virtual socketInitializer { | |
| _connectSocket(appGatewayId_, socket_, switchboard_); | |
| _connectSocket(appGatewayId_, socket_, switchboardId_); | |
| } | |
| /// @notice Initializes the socket | |
| /// @dev this function should be called even if deployed independently | |
| /// to avoid ownership and permission exploit | |
| /// @param appGatewayId_ The app gateway id | |
| /// @param socket_ The socket address | |
| /// @param switchboardId_ The switchboard id | |
| function initSocket( | |
| bytes32 appGatewayId_, | |
| address socket_, | |
| uint64 switchboardId_ | |
| ) external virtual socketInitializer onlyOwner { | |
| _connectSocket(appGatewayId_, socket_, switchboardId_); | |
| } |
🤖 Prompt for AI Agents
In contracts/protocol/base/PlugBase.sol around lines 76-88, initSocket is
external and only protected by socketInitializer, allowing any first caller to
permanently bind the plug; change the function visibility or add ownership/role
checks: either (preferred) integrate an ownership mechanism (e.g.,
Ownable2Step/OwnableRoles) and require onlyOwner or a specific role to call
initSocket, or make initSocket internal and expose a separate externally
callable initializer in derived contracts that enforces owner/role gating;
update constructor/initializers and documentation accordingly so
socket/appGatewayId/switchboardId can only be set by the authorized account.
| function processTrigger( | ||
| address plug_, | ||
| bytes32 triggerId_, | ||
| bytes calldata payload_, | ||
| bytes calldata overrides_ | ||
| ) external payable override { | ||
| // Simple implementation that just accepts the trigger | ||
| // In a real switchboard, this would process the trigger | ||
| } | ||
|
|
||
| function registerSwitchboard() external { | ||
| socket__.registerSwitchboard(); | ||
| function getTransmitter( | ||
| address sender_, | ||
| bytes32, | ||
| bytes calldata | ||
| ) external pure returns (address) { | ||
| return sender_; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify mock matches ISwitchboard signature
processTrigger and getTransmitter signatures must match ISwitchboard. A mismatch compiles within the test suite if interfaces diverged, but will hide issues. This should confirm parity.
🏁 Script executed:
#!/bin/bash
# Verify ISwitchboard signatures
rg -n -C2 'interface\s+ISwitchboard\b' --type=sol
rg -n -C2 'processTrigger\(' --type=sol
rg -n -C2 'getTransmitter\(' --type=solLength of output: 83
🏁 Script executed:
#!/bin/bash
# Search all Solidity files for interface and function definitions
rg -nP 'interface\s+ISwitchboard' -g '*.sol'
rg -nP 'function\s+processTrigger' -g '*.sol'
rg -nP 'function\s+getTransmitter' -g '*.sol'Length of output: 701
🏁 Script executed:
#!/bin/bash
# Print ISwitchboard interface signatures
rg -n -C2 'interface\s+ISwitchboard' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
rg -n -C2 'function\s+processTrigger' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
rg -n -C2 'function\s+getTransmitter' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
echo "----"
# Print MockFastSwitchboard signatures
rg -n -C2 'function\s+processTrigger' -g 'test/mock/MockFastSwitchboard.sol'
rg -n -C2 'function\s+getTransmitter' -g 'test/mock/MockFastSwitchboard.sol'Length of output: 1696
🏁 Script executed:
#!/bin/bash
# Extract processTrigger signature and surrounding for context
sed -n '25,50p' contracts/protocol/interfaces/ISwitchboard.sol
echo '---'
# Extract getTransmitter signature and surrounding for context
sed -n '38,60p' contracts/protocol/interfaces/ISwitchboard.solLength of output: 1097
Align mock to ISwitchboard signature
- In test/mock/MockFastSwitchboard.sol, change
function getTransmitter(address sender_, bytes32, bytes calldata) external pure returns (address)
to
function getTransmitter(address sender_, bytes32 payloadId_, bytes calldata transmitterSignature_) external view override returns (address)
to match the interface’s mutability and override.
🤖 Prompt for AI Agents
In test/mock/MockFastSwitchboard.sol around lines 48 to 64, the mock's
getTransmitter signature does not match the ISwitchboard interface: it currently
declares parameters anonymously, is pure, and lacks override; update the
function signature to use named parameters (payloadId_ and
transmitterSignature_), change mutability to view, and add the override keyword
so it reads: function getTransmitter(address sender_, bytes32 payloadId_, bytes
calldata transmitterSignature_) external view override returns (address); ensure
the function body still returns sender_.
| function testFuzzLargeProofs(bytes32 digest, uint256 proofSize) public { | ||
| vm.assume(digest != bytes32(0)); | ||
| vm.assume(proofSize >= 65 && proofSize <= 1000); // Reasonable proof sizes | ||
|
|
||
| // Create a large proof | ||
| bytes memory largeProof = new bytes(proofSize); | ||
| for (uint256 i = 0; i < proofSize; i++) { | ||
| largeProof[i] = bytes1(uint8(i % 256)); | ||
| } | ||
|
|
||
| vm.expectRevert(InvalidSignature.selector); | ||
| fastSwitchboard.attest(digest, largeProof); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for InvalidSignature.
Line 164 references InvalidSignature.selector, but InvalidSignature is not imported. This will cause a compilation error.
Import the error from the appropriate contract. If it's defined in FastSwitchboard:
+import {FastSwitchboard, InvalidSignature} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol";
-import {FastSwitchboard} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol";Or if it's in a common errors file, import from there. Verify the correct location and add the import statement.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function testFuzzLargeProofs(bytes32 digest, uint256 proofSize) public { | |
| vm.assume(digest != bytes32(0)); | |
| vm.assume(proofSize >= 65 && proofSize <= 1000); // Reasonable proof sizes | |
| // Create a large proof | |
| bytes memory largeProof = new bytes(proofSize); | |
| for (uint256 i = 0; i < proofSize; i++) { | |
| largeProof[i] = bytes1(uint8(i % 256)); | |
| } | |
| vm.expectRevert(InvalidSignature.selector); | |
| fastSwitchboard.attest(digest, largeProof); | |
| } | |
| // at the top of test/protocol/switchboards/FastSwitchboardTest.t.sol | |
| import {FastSwitchboard, InvalidSignature} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol"; |
🤖 Prompt for AI Agents
In test/protocol/switchboards/FastSwitchboardTest.t.sol around lines 154-166,
the test references InvalidSignature.selector but the error type is not imported
which causes a compilation failure; add an import for the InvalidSignature error
from the contract that declares it (either import it from the FastSwitchboard
contract file if it's declared there, or from the shared/common errors file
where it lives), update the top-of-file import block to include that symbol, and
ensure the import path matches the project structure so the test compiles.
| @@ -0,0 +1,88 @@ | |||
| // SPDX-License-Identifier: GPL-3.0-only | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove " copy" from filename.
The filename MessageSwitchboardTest.t copy.sol contains a " copy" suffix, indicating this is likely an accidental duplicate or improperly renamed file. This will cause confusion and potential issues with build systems.
Rename the file to:
MessageSwitchboardTest.t.sol
🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around line 1,
the filename includes an unintended " copy" suffix; rename the file to remove
the suffix so it becomes
test/protocol/switchboards/MessageSwitchboardTest.t.sol, update any
references/imports in the test suite or CI configs if present to the new
filename, and commit the rename so build systems and tooling no longer see the
duplicate.
| import "../../SetupTest.t.sol"; | ||
| import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing MessageSwitchboard import.
The test references MessageSwitchboard.SiblingNotFound (line 67) and MessageSwitchboard.WatcherNotFound (line 85) but doesn't import MessageSwitchboard. This will cause compilation failures.
Add the import:
import "../../SetupTest.t.sol";
import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol";
+import {MessageSwitchboard} from "../../../contracts/socket/switchboards/MessageSwitchboard.sol";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import "../../SetupTest.t.sol"; | |
| import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol"; | |
| import "../../SetupTest.t.sol"; | |
| import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol"; | |
| import {MessageSwitchboard} from "../../../contracts/socket/switchboards/MessageSwitchboard.sol"; |
🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around lines
4-5, the file fails to import MessageSwitchboard even though the test uses
MessageSwitchboard.SiblingNotFound and MessageSwitchboard.WatcherNotFound; add
an import statement for the MessageSwitchboard contract at the top of the file
(import the MessageSwitchboard Solidity file from the protocol/switchboards
folder, e.g. ../../protocol/switchboards/MessageSwitchboard.sol) so the compiler
can resolve those referenced error types.
| emit TriggerProcessed( | ||
| optConfig.chainSlug, | ||
| msgSbFees, | ||
| _createDigest(digestParams), | ||
| digestParams | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify TriggerProcessed event import.
The test expects the TriggerProcessed event but there's no visible import or definition for it in this file. Confirm this event is properly imported from a parent contract or MessageSwitchboard.
Run the following script to verify the event definition:
🏁 Script executed:
#!/bin/bash
# Find TriggerProcessed event definition
rg -n "event TriggerProcessed" --type=solLength of output: 27
🏁 Script executed:
#!/bin/bash
rg -n "event TriggerProcessed" --glob "*.sol"Length of output: 222
#!/bin/bash
# Locate MessageSwitchboardTest file(s)
fd MessageSwitchboardTest -e sol
# Inspect import statements in each test file
for f in $(fd MessageSwitchboardTest -e sol); do
echo "---- $f ----"
sed -n '1,50p' "$f"
done🏁 Script executed:
#!/bin/bash
# Locate MessageSwitchboardTest file(s)
files=$(rg -l "MessageSwitchboardTest" -g 'test/protocol/switchboards/*.sol')
echo "$files"
for f in $files; do
echo "---- $f ----"
head -n 50 "$f"
echo "--- Event definitions ---"
rg -n "event TriggerProcessed" "$f" || echo "No event in $f"
echo "--- Inheritance ---"
rg -nP "contract\s+MessageSwitchboardTest\s+is\s+\w+" "$f" || echo "No inheritance found in $f"
doneLength of output: 888
#!/bin/bash
for f in test/protocol/switchboards/*.sol; do
echo "---- $f ----"
head -n 100 "$f"
done#!/bin/bash
ls -1 test/protocol/switchboards
for f in test/protocol/switchboards/*.sol; do
echo "---- $f ----"
head -n 50 "$f"
done🏁 Script executed:
#!/bin/bash
ls -1 test/protocol/switchboards
for f in test/protocol/switchboards/*.sol; do
echo "---- $f ----"
head -n 50 "$f"
doneLength of output: 3602
Add missing TriggerProcessed event declaration/import
This should import or redeclare the TriggerProcessed event so vm.expectEmit can locate it in test/protocol/switchboards/MessageSwitchboardTest.t copy.sol.
🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around lines 36
to 41, the test emits TriggerProcessed but the event is not declared or imported
so vm.expectEmit cannot resolve it; add the TriggerProcessed event declaration
(matching the event signature used in production contracts) near the top of this
test file or import the contract/interface that defines TriggerProcessed, then
ensure the event signature exactly matches the emitted parameters so
vm.expectEmit can locate and compare the event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| address from_, | ||
| address to_, | ||
| uint256 amount_ | ||
| ) public override returns (bool) { | ||
| if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable(); | ||
|
|
||
| if (isNonceUsed[consumeFrom][nonce]) revert NonceUsed(); | ||
| isNonceUsed[consumeFrom][nonce] = true; | ||
| isApproved[consumeFrom][spender] = approval; | ||
| return (consumeFrom, spender, approval); | ||
| if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_); | ||
| return super.transferFrom(from_, to_, amount_); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whitelist RequestHandler as privileged spender to unblock RequestHandler.transferFrom
RequestHandler pulls credits during batch processing. Make it a privileged spender to avoid manual approvals.
- if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
-
- if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
+ if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
+ // Allow Watcher and RequestHandler to spend without pre-approval
+ if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) {
+ _approve(from_, msg.sender, amount_);
+ }
return super.transferFrom(from_, to_, amount_);This pairs with the RequestHandler fee transfer path.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| address from_, | |
| address to_, | |
| uint256 amount_ | |
| ) public override returns (bool) { | |
| if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable(); | |
| if (isNonceUsed[consumeFrom][nonce]) revert NonceUsed(); | |
| isNonceUsed[consumeFrom][nonce] = true; | |
| isApproved[consumeFrom][spender] = approval; | |
| return (consumeFrom, spender, approval); | |
| if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_); | |
| return super.transferFrom(from_, to_, amount_); | |
| } | |
| function transferFrom( | |
| address from_, | |
| address to_, | |
| uint256 amount_ | |
| ) public override returns (bool) { | |
| if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable(); | |
| // Allow Watcher and RequestHandler to spend without pre-approval | |
| if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) { | |
| _approve(from_, msg.sender, amount_); | |
| } | |
| return super.transferFrom(from_, to_, amount_); | |
| } |
| // todo: can be changed to set the default gas limit for each chain | ||
| if (queueParams_.overrideParams.gasLimit == 0) { | ||
| queueParams_.overrideParams.gasLimit = 10000000; | ||
| if (queueParams_.transaction.chainSlug == 5000) { | ||
| // Mantle default gas limit | ||
| queueParams_.overrideParams.gasLimit = 8_000_000_000; | ||
| } else if (queueParams_.transaction.chainSlug == 1329) { | ||
| // Sei default gas limit | ||
| queueParams_.overrideParams.gasLimit = 8_000_000; | ||
| } else if (queueParams_.transaction.chainSlug == 999) { | ||
| // HyperEVM default gas limit | ||
| queueParams_.overrideParams.gasLimit = 1_500_000; | ||
| } else { | ||
| queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mantle default gas limit is unrealistic (likely to revert).
8_000_000_000 gas will exceed typical block gas limits. This should be in the millions, not billions.
- if (queueParams_.transaction.chainSlug == 5000) {
- // Mantle default gas limit
- queueParams_.overrideParams.gasLimit = 8_000_000_000;
+ if (queueParams_.transaction.chainSlug == 5000) {
+ // Mantle default gas limit
+ queueParams_.overrideParams.gasLimit = 8_000_000;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // todo: can be changed to set the default gas limit for each chain | |
| if (queueParams_.overrideParams.gasLimit == 0) { | |
| queueParams_.overrideParams.gasLimit = 10000000; | |
| if (queueParams_.transaction.chainSlug == 5000) { | |
| // Mantle default gas limit | |
| queueParams_.overrideParams.gasLimit = 8_000_000_000; | |
| } else if (queueParams_.transaction.chainSlug == 1329) { | |
| // Sei default gas limit | |
| queueParams_.overrideParams.gasLimit = 8_000_000; | |
| } else if (queueParams_.transaction.chainSlug == 999) { | |
| // HyperEVM default gas limit | |
| queueParams_.overrideParams.gasLimit = 1_500_000; | |
| } else { | |
| queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit | |
| } | |
| // todo: can be changed to set the default gas limit for each chain | |
| if (queueParams_.overrideParams.gasLimit == 0) { | |
| if (queueParams_.transaction.chainSlug == 5000) { | |
| // Mantle default gas limit | |
| queueParams_.overrideParams.gasLimit = 8_000_000; | |
| } else if (queueParams_.transaction.chainSlug == 1329) { | |
| // Sei default gas limit | |
| queueParams_.overrideParams.gasLimit = 8_000_000; | |
| } else if (queueParams_.transaction.chainSlug == 999) { | |
| // HyperEVM default gas limit | |
| queueParams_.overrideParams.gasLimit = 1_500_000; | |
| } else { | |
| queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit | |
| } | |
| } |
🤖 Prompt for AI Agents
In contracts/evmx/watcher/precompiles/WritePrecompile.sol around lines 123-136,
the Mantle default gasLimit is set to 8_000_000_000 (billions) which is
unrealistic and will likely revert; change that value to a sensible
million-scale default (e.g., 8_000_000) and keep the same numeric-formatting
style, review other chain defaults for correctness, and run tests/quick local
deploy to ensure the new default does not exceed block limits.
| // todo: remove after game | ||
| // also this overrides a payload deadline hence an unexecuted payload can | ||
| // be executed by new added transmitters. need to fix this by marking req deadline or something. | ||
| if (r.requestFeesDetails.winningBid.transmitter == bid_.transmitter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove TODO and fix deadline override behavior
Reassigning transmitters currently “overrides a payload deadline” without safeguards. This should either:
- Track a request-level deadline and prevent execution after expiry, or
- Recompute deadlines deterministically per reassignment and enforce monotonicity.
Leave no TODOs in shipping code; implement one of the above and cover with tests.
| address watcherFeesPayer = r.requestFeesDetails.winningBid.transmitter == address(0) | ||
| ? r.requestFeesDetails.consumeFrom | ||
| : r.requestFeesDetails.winningBid.transmitter; | ||
| feesManager__().transferCredits(watcherFeesPayer, address(this), totalFees); | ||
| IERC20(address(feesManager__())).transferFrom(watcherFeesPayer, address(this), totalFees); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fee transfer will fail: RequestHandler is not an approved/privileged ERC20 spender
Calling transferFrom on the credits ERC20 from RequestHandler will require allowance for RequestHandler, which is never granted. _isWatcher(msg.sender) logic in Credit.sol won’t apply because msg.sender there is RequestHandler, not the Watcher.
Fix options:
- Whitelist RequestHandler as a privileged spender in Credit.sol’s transferFrom override (auto-approve when msg.sender == watcher__().requestHandler__()).
- Or avoid ERC20 transferFrom here and use a FeesManager method to block/debit credits atomically (preferred if available).
Minimal change (whitelist RequestHandler in Credit.sol):
// In Credit.sol -> transferFrom override
- if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
-
- if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
+ if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
+ // Allow Watcher and RequestHandler to pull without prior allowance
+ if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) {
+ _approve(from_, msg.sender, amount_);
+ }
return super.transferFrom(from_, to_, amount_);Also confirm the destination (address(this)) is correct and won’t trap credits without a settlement path.
🏁 Script executed:
#!/bin/bash
# Locate Credit.sol
rg -l "contract Credit" -g '*.sol'
# Inspect transferFrom override in Credit.sol
rg -n -C5 "function transferFrom" -g '*Credit.sol'
# Inspect isWatcher usage in Credit.sol
rg -n "isWatcher" -g '*Credit.sol'
# Inspect watcher__() accessor in Credit.sol
rg -n "function watcher__" -g '*Credit.sol'Length of output: 1057
🏁 Script executed:
#!/bin/bash
# Dump context around transferFrom override
sed -n '180,260p' contracts/evmx/fees/Credit.sol
# Search for watcher__ and requestHandler references
rg -n "watcher__" -g 'contracts/evmx/fees/Credit.sol'
rg -n "requestHandler" -g 'contracts/evmx/fees/Credit.sol'Length of output: 3295
🏁 Script executed:
#!/bin/bash
# Locate requestHandler__ accessor in watcher contracts
rg -n "requestHandler__" -g 'contracts/evmx/watcher/*.sol'Length of output: 1612
Whitelist RequestHandler in Credit.sol or use FeesManager debit
Transfer will revert: Credit.sol’s transferFrom override only auto-approves watchers, not the RequestHandler (msg.sender).
Fix options:
- In
contracts/evmx/fees/Credit.sol’stransferFrom, change:- if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_); + if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) _approve(from_, msg.sender, amount_);
- Or replace
IERC20(...).transferFrominRequestHandler.solwith a FeesManager debit method to atomically block/debit credits.
Also confirmaddress(this)won’t trap credits without a settlement path.
Committable suggestion skipped: line range outside the PR's diff.
| SOLANA_MAINNET = ChainId.SOLANA_MAINNET, | ||
| SOLANA_DEVNET = ChainId.SOLANA_DEVNET, | ||
| SONIC = ChainId.SONIC, | ||
| KATANA = ChainId.KATANA, | ||
| HYPEREVM = ChainId.HYPEREVM, | ||
| SEI = ChainId.SEI, | ||
| ZERO = ChainId.ZERO, | ||
| ZKSYNC = ChainId.ZKSYNC, | ||
| ARENA_Z = ChainId.ARENA_Z, | ||
| INK = ChainId.INK, | ||
| BERA = ChainId.BERA, | ||
| B3 = ChainId.B3, | ||
| UNICHAIN = ChainId.UNICHAIN, | ||
| MONAD_TESTNET = ChainId.MONAD_TESTNET, | ||
| SCROLL = ChainId.SCROLL, | ||
| SONEIUM = ChainId.SONEIUM, | ||
| SWELLCHAIN = ChainId.SWELLCHAIN, | ||
| WORLD_CHAIN = ChainId.WORLD_CHAIN, | ||
| FLOW = ChainId.FLOW, | ||
| CAMP = ChainId.CAMP, | ||
| PLUME = ChainId.PLUME, | ||
| RISE_TESTNET = ChainId.RISE_TESTNET, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify new slugs are fully wired (ChainId and mappings).
These slugs must exist in ChainId and be present in chainSlugToId/chainSlugToHardhatChainName mappings. Missing entries will break deployments and lookups.
🏁 Script executed:
#!/bin/bash
# Ensure each new slug exists in ChainId and mapping files.
set -euo pipefail
NEW_SLUGS=(
SOLANA_MAINNET SOLANA_DEVNET SONIC KATANA HYPEREVM SEI ZERO ZKSYNC ARENA_Z INK
BERA B3 UNICHAIN MONAD_TESTNET SCROLL SONEIUM SWELLCHAIN WORLD_CHAIN FLOW CAMP
PLUME RISE_TESTNET
)
echo "Checking ChainId definitions..."
for s in "${NEW_SLUGS[@]}"; do
rg -nC2 -g 'src/**' "enum\\s+ChainId|\\b$s\\b" src/chain-enums/chainId.ts >/dev/null || echo "Missing in ChainId: $s"
done
echo "Checking chainSlugToId mapping..."
for s in "${NEW_SLUGS[@]}"; do
rg -nC1 -g 'src/**' "ChainSlug\\.$s\\s*:" src/chain-enums/chainSlugToId.ts >/dev/null || echo "Missing in chainSlugToId: $s"
done
echo "Checking chainSlugToHardhatChainName mapping..."
for s in "${NEW_SLUGS[@]}"; do
rg -nC1 -g 'src/**' "ChainSlug\\.$s\\s*:" src/chain-enums/chainSlugToHardhatChainName.ts >/dev/null || echo "Missing in chainSlugToHardhatChainName: $s"
doneLength of output: 1919
Define new ChainSlugs in mapping files
All slugs added in src/chain-enums/chainSlug.ts must be defined in src/chain-enums/chainSlugToId.ts and src/chain-enums/chainSlugToHardhatChainName.ts or lookups will break. Add entries for:
SOLANA_MAINNET, SOLANA_DEVNET, SONIC, KATANA, HYPEREVM, SEI, ZERO, ZKSYNC, ARENA_Z, INK, BERA, B3, UNICHAIN, MONAD_TESTNET, SCROLL, SONEIUM, SWELLCHAIN, WORLD_CHAIN, FLOW, CAMP, PLUME, RISE_TESTNET.
🤖 Prompt for AI Agents
In src/chain-enums/chainSlug.ts around lines 62 to 84, multiple new chain slugs
were added but corresponding entries are missing in
src/chain-enums/chainSlugToId.ts and
src/chain-enums/chainSlugToHardhatChainName.ts which will break lookups; add
mapping entries for each of the following slugs in both files: SOLANA_MAINNET,
SOLANA_DEVNET, SONIC, KATANA, HYPEREVM, SEI, ZERO, ZKSYNC, ARENA_Z, INK, BERA,
B3, UNICHAIN, MONAD_TESTNET, SCROLL, SONEIUM, SWELLCHAIN, WORLD_CHAIN, FLOW,
CAMP, PLUME, RISE_TESTNET, ensuring each slug maps to the correct ChainId
numeric value in chainSlugToId.ts and to the proper hardhat network name (or
null/undefined if not applicable) in chainSlugToHardhatChainName.ts; follow
existing mapping format and ordering, run tests, and update any types or exports
if required.
| uint256 withdrawAmount = feesManager.balanceOf(transmitterEOA); | ||
| withdrawAmount = withdrawAmount - feesAmount; | ||
| withdrawCredits(transmitterEOA, withdrawAmount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid potential underflow on withdrawAmount
withdrawAmount = balanceOf(transmitterEOA) - feesAmount will revert if balance <= feesAmount. This should guard or pre-seed sufficient balance to make the test deterministic.
Apply one of:
- Assert and seed: ensure balance > feesAmount and mint/deposit to transmitter if needed.
- Guard and skip: if balance <= feesAmount, set withdrawAmount = 0 and expect revert accordingly.
🤖 Prompt for AI Agents
In test/evmx/FeesTest.t.sol around lines 55-57, the subtraction withdrawAmount =
balanceOf(transmitterEOA) - feesAmount can underflow if the balance is <=
feesAmount; either ensure the test pre-seeds the transmitterEOA with sufficient
funds (mint/deposit so balance > feesAmount) and assert that precondition before
computing withdrawAmount, or guard the computation by checking the balance and
setting withdrawAmount = 0 (and adjust the test to expect a revert or skip the
withdraw) to make the test deterministic and avoid underflow.
| uint256 expiryTime = expiryTime + 100; | ||
| hoax(watcherAddress); | ||
| vm.expectEmit(true, true, true, true); | ||
| emit SchedulePrecompile.ExpiryTimeSet(expiryTime); | ||
| schedulePrecompile.setExpiryTime(expiryTime); | ||
|
|
||
| hoax(nonOwner); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shadowed variable uses itself before initialization
uint256 expiryTime = expiryTime + 100; reads the uninitialized local. This should reference the state var via a different local name.
- uint256 expiryTime = expiryTime + 100;
+ uint256 newExpiryTime = expiryTime + 100;
...
- emit SchedulePrecompile.ExpiryTimeSet(expiryTime);
- schedulePrecompile.setExpiryTime(expiryTime);
+ emit SchedulePrecompile.ExpiryTimeSet(newExpiryTime);
+ schedulePrecompile.setExpiryTime(newExpiryTime);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uint256 expiryTime = expiryTime + 100; | |
| hoax(watcherAddress); | |
| vm.expectEmit(true, true, true, true); | |
| emit SchedulePrecompile.ExpiryTimeSet(expiryTime); | |
| schedulePrecompile.setExpiryTime(expiryTime); | |
| hoax(nonOwner); | |
| uint256 newExpiryTime = expiryTime + 100; | |
| hoax(watcherAddress); | |
| vm.expectEmit(true, true, true, true); | |
| emit SchedulePrecompile.ExpiryTimeSet(newExpiryTime); | |
| schedulePrecompile.setExpiryTime(newExpiryTime); | |
| hoax(nonOwner); |
🤖 Prompt for AI Agents
In test/evmx/Watcher.t.sol around lines 787 to 793, the line "uint256 expiryTime
= expiryTime + 100;" reads the local variable before it's initialized; replace
it by reading the contract/state variable into a new local name (e.g., uint256
newExpiry = expiryTime + 100) and use that new local for the expectEmit and
setExpiryTime calls so the state value is referenced correctly and the local is
initialized.
| function initSocket( | ||
| bytes32 appGatewayId_, | ||
| address socket_, | ||
| uint64 switchboardId_ | ||
| ) external override { | ||
| appGatewayId = appGatewayId_; | ||
| socket__ = ISocket(socket_); | ||
| initSocketCalled = true; | ||
|
|
||
| socket__.connect(appGatewayId_, switchboardId_); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set switchboardId state in initSocket
switchboardId storage var is never updated, causing stale/zero reads. This should assign it when initializing.
function initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
) external override {
appGatewayId = appGatewayId_;
socket__ = ISocket(socket_);
+ switchboardId = switchboardId_;
initSocketCalled = true;
socket__.connect(appGatewayId_, switchboardId_);
}
```<!-- review_comment_end -->
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
function initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
) external override {
appGatewayId = appGatewayId_;
socket__ = ISocket(socket_);
switchboardId = switchboardId_;
initSocketCalled = true;
socket__.connect(appGatewayId_, switchboardId_);
}🤖 Prompt for AI Agents
In test/mock/MockPlug.sol around lines 20 to 31, the initSocket function sets
appGatewayId and socket__ but never assigns the switchboardId storage variable,
leaving it stale/zero; update the function to assign switchboardId =
switchboardId_ (ensure the storage variable name matches the contract field) so
the initialized value is persisted before calling socket__.connect.
| payloadPointer = | ||
| (uint160(srcSocketConfig_.chainSlug) << 120) | | ||
| (uint160(uint64(uint256(triggerId))) << 80) | | ||
| payloadCounter; | ||
|
|
||
| bytes32 payloadId = createPayloadId( | ||
| payloadPointer, | ||
| dstSocketConfig_.messageSwitchboard.switchboardId(), | ||
| dstSocketConfig_.chainSlug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payloadPointer packing uses 64-bit field for a 40-bit slot (overwrites high bits)
The middle slot is 40 bits, but you shift a 64-bit value into it, corrupting the high 24 bits and overlapping the top slot. This should constrain to 40 bits.
- payloadPointer =
- (uint160(srcSocketConfig_.chainSlug) << 120) |
- (uint160(uint64(uint256(triggerId))) << 80) |
- payloadCounter;
+ payloadPointer =
+ (uint160(srcSocketConfig_.chainSlug) << 120) |
+ (uint160(uint40(uint64(uint256(triggerId)))) << 80) |
+ uint160(payloadCounter);Optionally add assertions to ensure values fit their bit ranges.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| payloadPointer = | |
| (uint160(srcSocketConfig_.chainSlug) << 120) | | |
| (uint160(uint64(uint256(triggerId))) << 80) | | |
| payloadCounter; | |
| bytes32 payloadId = createPayloadId( | |
| payloadPointer, | |
| dstSocketConfig_.messageSwitchboard.switchboardId(), | |
| dstSocketConfig_.chainSlug | |
| payloadPointer = | |
| (uint160(srcSocketConfig_.chainSlug) << 120) | | |
| (uint160(uint40(uint64(uint256(triggerId)))) << 80) | | |
| uint160(payloadCounter); | |
| bytes32 payloadId = createPayloadId( | |
| payloadPointer, | |
| dstSocketConfig_.messageSwitchboard.switchboardId(), | |
| dstSocketConfig_.chainSlug |
| contract IdUtilsTest is Test { | ||
| function testCreatePayloadId() public pure { | ||
| uint160 payloadPointer = 12345; | ||
| uint64 switchboardId = 67890; | ||
| uint32 chainSlug = 1; | ||
|
|
||
| bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); | ||
|
|
||
| // Verify the structure | ||
| uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); | ||
| uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); | ||
| uint160 payloadPointerFromId = uint160(uint256(payloadId)); | ||
|
|
||
| assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); | ||
| assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); | ||
| assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); | ||
| } | ||
|
|
||
| function testCreatePayloadIdWithZeroValues() public pure { | ||
| bytes32 payloadId = createPayloadId(0, 0, 0); | ||
|
|
||
| assertEq(payloadId, bytes32(0), "Payload ID should be zero for zero inputs"); | ||
| } | ||
|
|
||
| function testCreatePayloadIdWithMaxValues() public pure { | ||
| uint160 maxPayloadPointer = type(uint160).max; | ||
| uint64 maxSwitchboardId = type(uint64).max; | ||
| uint32 maxChainSlug = type(uint32).max; | ||
|
|
||
| bytes32 payloadId = createPayloadId(maxPayloadPointer, maxSwitchboardId, maxChainSlug); | ||
|
|
||
| // Verify the structure | ||
| uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); | ||
| uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); | ||
| uint160 payloadPointerFromId = uint160(uint256(payloadId)); | ||
|
|
||
| assertEq(chainSlugFromId, maxChainSlug, "Chain slug should match"); | ||
| assertEq(switchboardIdFromId, maxSwitchboardId, "Switchboard ID should match"); | ||
| assertEq(payloadPointerFromId, maxPayloadPointer, "Payload pointer should match"); | ||
| } | ||
|
|
||
| function testCreatePayloadIdFuzz( | ||
| uint160 payloadPointer, | ||
| uint64 switchboardId, | ||
| uint32 chainSlug | ||
| ) public pure { | ||
| bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); | ||
|
|
||
| // Verify the structure | ||
| uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); | ||
| uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); | ||
| uint160 payloadPointerFromId = uint160(uint256(payloadId)); | ||
|
|
||
| assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); | ||
| assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); | ||
| assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove pure from test functions (compile error)
Test functions call assertions/cheatcodes/external calls; marking them pure is invalid and will not compile. This should be non-pure public.
-contract IdUtilsTest is Test {
- function testCreatePayloadId() public pure {
+contract IdUtilsTest is Test {
+ function testCreatePayloadId() public {
@@
- function testCreatePayloadIdWithZeroValues() public pure {
+ function testCreatePayloadIdWithZeroValues() public {
@@
- function testCreatePayloadIdWithMaxValues() public pure {
+ function testCreatePayloadIdWithMaxValues() public {
@@
- function testCreatePayloadIdFuzz(
+ function testCreatePayloadIdFuzz(
uint160 payloadPointer,
uint64 switchboardId,
uint32 chainSlug
- ) public pure {
+ ) public {
@@
-contract ConvertersTest is Test {
- function testToBytes32Format() public pure {
+contract ConvertersTest is Test {
+ function testToBytes32Format() public {
@@
- function testToBytes32FormatWithZeroAddress() public pure {
+ function testToBytes32FormatWithZeroAddress() public {
@@
- function testFromBytes32Format() public pure {
+ function testFromBytes32Format() public {
@@
- function testFromBytes32FormatWithZeroAddress() public pure {
+ function testFromBytes32FormatWithZeroAddress() public {
@@
- function testFromBytes32FormatWithInvalidAddress() public {
+ function testFromBytes32FormatWithInvalidAddress() public {
@@
- function testFromBytes32FormatWithMaxValidAddress() public pure {
+ function testFromBytes32FormatWithMaxValidAddress() public {
@@
- function testConvertersRoundTrip() public pure {
+ function testConvertersRoundTrip() public {
@@
- function testConvertersFuzz(address addr) public pure {
+ function testConvertersFuzz(address addr) public {Keep fromBytes32FormatWrapper as external pure.
Also applies to: 75-149
🤖 Prompt for AI Agents
In test/Utils.t.sol around lines 12-69 (and also apply the same change to lines
75-149), the test functions are declared as `public pure` which causes
compilation errors because tests use assertions/cheatcodes/external calls;
remove the `pure` specifier so the functions are plain `public` (i.e., change
signatures from `function ... public pure` to `function ... public`). Keep any
helper `fromBytes32FormatWrapper` as `external pure` as noted.
fix: nonce, s3 config overrides
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
package.json (1)
25-25: Guard.envload and avoid non-POSIXsource.npm scripts run under
/bin/sh, sosourceis undefined and the command fails, and it still crashes whenever.envis absent. This should wrap the load in a file check and run inside bash:bash -c '[[ -f .env ]] && source .env; bash trace.sh'.
🧹 Nitpick comments (2)
test/apps/app-gateways/counter/CounterAppGateway.sol (2)
59-67: Remove unnecessary blank lines.Lines 65 and 67 add unnecessary whitespace. Remove them for cleaner code.
function incrementCounters(address[] memory instances_) public async { incremented = false; // the increase function is called on given list of instances for (uint256 i = 0; i < instances_.length; i++) { ICounter(instances_[i]).increase(); } - onCompleteData = abi.encodeWithSelector(this.onIncrementComplete.selector); - } + }
168-171: Improve parameter naming and use custom errors.This function has multiple issues:
- The first four parameters are unnamed, making the function signature unclear. Name them for better readability.
- The revert uses a string message instead of a custom error, which is less gas-efficient.
- function onTransfer(uint32, address, uint256, uint256, bytes memory data_) public { + function onTransfer( + uint32 chainSlug_, + address token_, + uint256 amount_, + uint256 msgId_, + bytes memory data_ + ) public { - if (msg.sender != address(feesManager__())) revert("Only fees manager"); + if (msg.sender != address(feesManager__())) revert OnlyFeesManager(); feesManagerSwitch = abi.decode(data_, (bool)); }You'll also need to define the custom error at the contract level:
error OnlyFeesManager();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (3)
hardhat-scripts/s3Config/buildConfig.ts(3 hunks)package.json(4 hunks)test/apps/app-gateways/counter/CounterAppGateway.sol(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- hardhat-scripts/s3Config/buildConfig.ts
🔇 Additional comments (1)
test/apps/app-gateways/counter/CounterAppGateway.sol (1)
69-73: Verify the behavior of calling async functions from non-async context.This function calls
incrementCounters, which is markedasync, butincrementCountersMultipleitself is not. In the actual system, calling an async function from a non-async context might not work as expected. Verify that this pattern functions correctly for test purposes.
| function onIncrementComplete() public { | ||
| incremented = true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add access control to callback function.
This callback lacks the onlyPromises modifier, allowing anyone to call it and set incremented = true. Other callbacks in this contract (like setCounterValues at line 105 and resolveSchedule at line 130) properly use onlyPromises. This should follow the same pattern.
- function onIncrementComplete() public {
+ function onIncrementComplete() public onlyPromises {
incremented = true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function onIncrementComplete() public { | |
| incremented = true; | |
| } | |
| function onIncrementComplete() public onlyPromises { | |
| incremented = true; | |
| } |
🤖 Prompt for AI Agents
In test/apps/app-gateways/counter/CounterAppGateway.sol around lines 75 to 77,
the onIncrementComplete callback is missing the onlyPromises access-control
modifier and therefore anyone can call it to set incremented = true; update the
function signature to include the onlyPromises modifier (e.g., change to
function onIncrementComplete() public onlyPromises) so it follows the same
access control pattern used by setCounterValues and resolveSchedule and prevent
unauthorized callers from toggling incremented.
Summary by CodeRabbit
New Features
Refactor
Tests
Chores